<html>

<head>
    <meta charset="utf8">
</head>

<body>
    <style>
        html,
        body {
            margin: 0;
            padding: 0;
            height: 100%;
            background-color: #f2e3c9;
        }

        canvas {
            position: absolute;
            left: 0;
            top: 0;
        }
    </style>
    <canvas id="canvas1"></canvas>
    <canvas id="canvas2"></canvas>

    <script>
        function bezier(p0, p1, p2, p3) {
            return function (t) {
                let a = (1 - t) ** 3;
                let b = (1 - t) ** 2 * t * 3;
                let c = (1 - t) * t * t * 3;
                let d = t * t * t;
                return a * p0 + b * p1 + c * p2 + d * p3;
            };
        }

        function expToRect(r, e) {
            return {
                a: r * Math.cos(e),
                b: r * Math.sin(e)
            };
        }

        function rectToExp(a, b) {
            return {
                r: Math.sqrt(a * a + b * b),
                e: Math.atan2(b, a)
            };
        }

        function integrate(fn, diff) {
            const result = { a: 0, b: 0 };
            const dt = 1 / diff;

            for (let i = 0; i <= 1; i += dt) {
                const { a, b } = fn(i);
                result.a += a * dt; // 实部
                result.b += b * dt; // 虚部
            }
            return result;
        }

        function calcSeries(fn, diff, n) {
            const u = Math.PI * 2 * n;
            const { a, b } = integrate(t => {
                const { r, e } = fn(t);
                return expToRect(r, e - u * t);
            }, diff);
            return { init: rectToExp(a, b), speed: u };
        }

        const bx1 = bezier(0, 100, 230, 0);
        const by1 = bezier(0, -100, 50, 230);
        const bx2 = bezier(0, -230, -100, 0);
        const by2 = bezier(230, 50, -100, 0);

        function drawHeart(t) {
            const s = (t % 0.5) / 0.5;
            const x = t < 0.5 ? bx1(s) : bx2(s);
            const y = t < 0.5 ? by1(s) : by2(s);

            return rectToExp(x, y);
        }

        const canvas1 = document.getElementById("canvas1");
        const canvas2 = document.getElementById("canvas2");
        const context1 = canvas1.getContext("2d");
        const context2 = canvas2.getContext("2d");
        const series = [];
        const first = calcSeries(drawHeart, 100, 0);
        const { a, b } = expToRect(first.init);
        const maxN = 100;
        let curN = 1;
        let scale = 1;
        let lastX = a;
        let lastY = b;

        canvas1.width = canvas2.width = window.innerWidth;
        canvas1.height = canvas2.height = window.innerHeight;
        context1.translate(canvas1.width / 2, canvas1.height / 2);
        context2.translate(canvas2.width / 2, canvas2.height / 2);
        context1.strokeStyle = "rgb(127,0,127)";

        function drawSeries(series, t, draw) {
            const w = canvas2.width;
            const h = canvas2.height;
            context2.clearRect(-w, -h, w * 2, h * 2);
            context2.lineWidth = 1;

            // draw lines
            context2.beginPath();
            context2.moveTo(0, 0);
            for (let i = 0, x = 0, y = 0; i < series.length; i++) {
                const { init, speed } = series[i];
                const { a, b } = expToRect(init.r, init.e + speed * t);
                const s = i < series.length - 1 ? 1 : scale;

                context2.lineTo((x += a * s), (y += b * s));
            }
            context2.strokeStyle = "blue";
            context2.stroke();
            context2.closePath();

            // draw dots
            for (let i = 0, x = 0, y = 0; i < series.length; i++) {
                const { init, speed } = series[i];
                const { a, b } = expToRect(init.r, init.e + speed * t);
                const s = i < series.length - 1 ? 1 : scale;

                context2.beginPath();
                context2.arc(x, y, 2, 0, 2 * Math.PI);
                (x += a * s), (y += b * s);
                context2.fillStyle = "rgba(255,55,55,0.5)";
                context2.fill();
                context2.closePath();
            }

            // draw circles
            for (let i = 0, x = 0, y = 0; i < series.length; i++) {
                const { init, speed } = series[i];
                const { a, b } = expToRect(init.r, init.e + speed * t);
                const s = i < series.length - 1 ? 1 : scale;

                context2.beginPath();
                context2.arc(x, y, init.r * s, 0, 2 * Math.PI);
                (x += a * s), (y += b * s);
                context2.strokeStyle = "rgba(232,100,124,0.8)";
                context2.stroke();
                context2.closePath();
            }
            // draw shape
            if (draw) {
                let x = 0;
                let y = 0;
                for (let i = 0; i < series.length; i++) {
                    const { init, speed } = series[i];
                    const { a, b } = expToRect(init.r, init.e + speed * t);
                    const s = i < series.length - 1 ? 1 : scale;
                    x += a * s;
                    y += b * s;
                }
                context1.beginPath();
                context1.moveTo(lastX, lastY);
                context1.lineTo((lastX = x), (lastY = y));
                context1.strokeStyle = "rgb(255,0,0)";
                context1.lineWidth = 3;
                context1.stroke();
                context1.closePath();
            }
        }

        function startLoop(callback) {
            let t = 0;
            let last = Date.now() * 0.001;

            window.requestAnimationFrame(function loop() {
                const now = Date.now() * 0.001;
                const dt = Math.min(now - last, 1 / 30);
                callback(dt, (t += dt));
                last = now;
                window.requestAnimationFrame(loop);
            });
        }

        startLoop((dt, t) => {
            dt *= 0.2;
            t *= 0.2;

            if (curN < maxN) {
                scale += t * t * 100 * dt;
                if (scale >= 1) {
                    scale = scale % 1;
                    series.push(calcSeries(drawHeart, 1000, curN));
                    curN = curN > 0 ? -curN : -curN + 1;
                }
            }
            drawSeries(series, t % 1, curN === maxN);
        });

    </script>
</body>

</html>